/*
 $Id$

 Copyright (C) 2006-2007 by David Cotton

 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation; either version 2 of the License, or (at your option) any later
 version.

 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
 Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package fr.free.jchecs.ai;

import static fr.free.jchecs.core.board.BoardFactory.State.EMPTY;
import static fr.free.jchecs.core.board.BoardFactory.Type.FASTEST;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import fr.free.jchecs.core.board.BoardFactory;
import fr.free.jchecs.core.move.Move;
import fr.free.jchecs.core.move.MoveGenerator;

/**
 * Base implementation of AI engines for chess.
 * 
 * @author David Cotton
 */
abstract class AbstractEngine implements Engine {
	/** Random number generator. */
	protected static final Random RANDOMIZER = new Random();

	/** Value of a Mate */
	protected static final int MATE_VALUE = Integer.MIN_VALUE / 2;

	/** Model for cutting the openings according to the records ';'. */
	static final Pattern SPLITTER = Pattern.compile(";");

	/** Buffer openings. */
	private static Map<Integer, int[]> S_openings;
	static {
		final Thread preload = new Thread(new Runnable() {
			/**
			 * Background to mask the loading time ...
			 */
			public void run() {
				getFromOpenings(BoardFactory.valueOf(FASTEST, EMPTY));
			}
		});
		preload.setPriority(Thread.MIN_PRIORITY);
		preload.start();
	}

	/** Lower limit of the depth of research. */
	private final int _minimalSearchDepth;

	/** High limit depth research. */
	private final int _maximalSearchDepth;

	/** Total time spent in treatment by the engine. */
	private long _elapsedTime;

	/** Total half moves rated by the engine. */
	private int _halfmoveCount;

	/** Function assessment tools used by the engine. */
	private Heuristic _heuristic;

	/** Sorting function of the movements. */
	private Comparator<Move> _moveSorter;

	/** Flag indicating the activation of the opening book. */
	private boolean _openingsEnabled;

	/** Score of the last movement. */
	private int _score;

	/** Limit the search depth (half-moves). */
	private int _searchDepthLimit;

	/**
	 * Instantiates a new engine AI.
	 * 
	 * @param pProfMin
	 *            Lower limit of the depth of research (>= 1).
	 * @param pProfMax
	 *            High limit depth research (>= pProfMin).
	 * @param pProfDef
	 *            Default limit of search depth ([pProfMin, pProfMax]).
	 */
	protected AbstractEngine(final int pProfMin, final int pProfMax,
			final int pProfDef) {
		assert pProfMin >= 1;
		assert pProfMax >= pProfMin;

		_minimalSearchDepth = pProfMin;
		_maximalSearchDepth = pProfMax;

		setSearchDepthLimit(pProfDef);

		setHeuristic(new MobilityHeuristic());
		setMoveSorter(new StaticMoveSorter());
		setOpeningsEnabled(true);
	}

	/**
	 * Adds a duration (ms) to total processing time by the engine.
	 * 
	 * @param pDuree
	 *            Duration in ms to add.
	 */
	protected final void addElapsedTime(final long pDuree) {
		assert pDuree >= 0;

		_elapsedTime += pDuree;
	}

	/**
	 * Adds a count half moves the number of half moves evaluated by engine.
	 * 
	 * @param pNombre
	 *            Number of half moves to add.
	 */
	protected final void addHalfmove(final int pNombre) {
		assert pNombre >= 0;

		_halfmoveCount += pNombre;
	}

	/**
	 * Returns the total time spent in treatment by the engine.
	 * 
	 * @return Total processing time by the engine (in ms).
	 */
	public final long getElapsedTime() {
		assert _elapsedTime >= 0;
		return _elapsedTime;
	}

	/**
	 * Reference movement corresponding to a position in the library opening.
	 * 
	 * @param pEtat
	 *            State of play
	 * @return Corresponding movement (or null)
	 */
	static final synchronized Move getFromOpenings(final MoveGenerator pEtat) {
		assert pEtat != null;

		Move res = null;

		if (S_openings == null) {
			final InputStream is = Engine.class
					.getResourceAsStream("jchecs.opn");
			if (is != null) {
				S_openings = new HashMap<Integer, int[]>();
				DataInputStream in = null;
				try {
					in = new DataInputStream(new GZIPInputStream(is));
					while (in.available() > 0) {
						final int nb = in.readByte();
						assert (nb > 0) && (nb <= 5);
						final int[] mvtsId = new int[nb];
						final int cle = in.readInt();
						for (int i = 0; i < nb; i++) {
							mvtsId[i] = (in.readUnsignedShort() << 8)
									+ in.readUnsignedByte();
							assert (mvtsId[i] & 0xFF000000) == 0;
						}
						S_openings.put(Integer.valueOf(cle), mvtsId);
					}
				} catch (final EOFException e) {
					// Pas grave, le coup sera calculé...
				} catch (final IOException e) {
					// Pas grave, le coup sera calculé...
					assert false;
				} finally {
					if (in != null) {
						try {
							in.close();
						} catch (final IOException e1) {
							// On aura essayé :-)
						}
					}
				}
			}
		}

		int[] ids = null;
		if (S_openings != null) {
			ids = S_openings.get(Integer.valueOf(pEtat.hashCode()));
		}
		if (ids != null) {
			res = Move.valueOf(ids[RANDOMIZER.nextInt(ids.length)]);
			// Les hashcodes n'étant pas infaïbles, il vaut mieux valider le
			// mouvement obtenu...
			boolean erreurHashcode = true;
			for (final Move mvt : pEtat.getValidMoves(pEtat.isWhiteActive())) {
				if (mvt.equals(res)) {
					erreurHashcode = false;
					break;
				}
			}
			if (erreurHashcode) {
				res = null;
			}
		}

		return res;
	}

	/**
	 * Returns the total number of half-moves to be evaluated by the engine.
	 * 
	 * @return Total half moves evaluated by the engine.
	 */
	public final int getHalfmoveCount() {
		assert _halfmoveCount >= 0;
		return _halfmoveCount;
	}

	/**
	 * Reference the evaluation function used by the engine.
	 * 
	 * @return Evaluation function used.
	 */
	public final Heuristic getHeuristic() {
		assert _heuristic != null;
		return _heuristic;
	}

	/**
	 * Reference upper limit of the depth of research supported by the engine.
	 * 
	 * @return High limit depth research (>= getMinimalSearchDepth()).
	 */
	public final int getMaximalSearchDepth() {
		assert _maximalSearchDepth >= _searchDepthLimit;
		return _maximalSearchDepth;
	}

	/**
	 * Reference lower limit of the depth of research supported by the engine.
	 * 
	 * @return Lower limit of the depth of research (>= 1).
	 */
	public final int getMinimalSearchDepth() {
		assert _minimalSearchDepth <= _searchDepthLimit;
		return _minimalSearchDepth;
	}

	/**
	 * Research a movement responding to a state of the chessboard.
	 * 
	 * @param pEtat
	 *            State of the chessboard.
	 * @return Movement found.
	 */
	public final synchronized Move getMoveFor(final MoveGenerator pEtat) {
		assert pEtat != null;

		final long debut = System.currentTimeMillis();

		Move res = null;

		setScore(0);

		if (_openingsEnabled && (pEtat.getFullmoveNumber() < 20)) {
			res = getFromOpenings(pEtat);
		}

		if (res == null) {
			// Calcul du meilleur coup...
			final Move[] coups = pEtat.getValidMoves(pEtat.isWhiteActive());
			assert coups.length > 0;

			res = searchMoveFor(pEtat, coups);
		}

		final long duree = System.currentTimeMillis() - debut;
		addElapsedTime(duree);

		assert res != null;
		return res;
	}

	/**
	 * Reference the sorting function of the movements.
	 * 
	 * @return Sorting function of the movements.
	 */
	public final Comparator<Move> getMoveSorter() {
		assert _moveSorter != null;
		return _moveSorter;
	}

	/**
	 * Returns the score calculated by the last movement.
	 * 
	 * @return Score of the last movement.
	 */
	public final int getScore() {
		return _score;
	}

	/**
	 * Reference value limit the search depth (half-moves).
	 * 
	 * @return Limit the search depth ([getMinimalSearchDepth(),
	 *         getMaximalSearchDepth()]).
	 */
	public final int getSearchDepthLimit() {
		assert _searchDepthLimit >= _minimalSearchDepth;
		return _searchDepthLimit;
	}

	/**
	 * Indicates whether to use the opening book is selected.
	 * 
	 * @return "true" if the openings are used, false otherwise.
	 */
	public final boolean isOpeningsEnabled() {
		return _openingsEnabled;
	}

	/**
	 * Body search of "better" half-move to a state of the board.
	 * 
	 * @param pEtat
	 *            State of the chessboard.
	 * @param pCoups
	 *            List of valid initial movement.
	 * @return Movement found.
	 */
	protected abstract Move searchMoveFor(final MoveGenerator pEtat,
			final Move[] pCoups);

	/**
	 * Modifies the evaluation function used by the engine.
	 * 
	 * @param pHeuristique
	 *            New evaluation function to use.
	 */
	public final void setHeuristic(final Heuristic pHeuristique) {
		assert pHeuristique != null;

		_heuristic = pHeuristique;
	}

	/**
	 * Alters the function of totally ordered movements.
	 * 
	 * @param pComparateur
	 *            New function sort of movement.
	 */
	public final void setMoveSorter(final Comparator<Move> pComparateur) {
		assert pComparateur != null;

		_moveSorter = pComparateur;
	}

	/**
	 * Enables / disables the use of the library opening.
	 * 
	 * @param pActif
	 *            A "true " to enable the use of openings, "false" otherwise.
	 */
	public final void setOpeningsEnabled(final boolean pActif) {
		_openingsEnabled = pActif;
	}

	/**
	 * Feeds the score calculated by the last movement.
	 * 
	 * @param pScore
	 *            Score of the last movement.
	 */
	protected final void setScore(final int pScore) {
		_score = pScore;
	}

	/**
	 * Food value of the limit of search depth (in half moves).
	 * 
	 * @param pLimite
	 *            Limit the search depth ([getMinimalSearchDepth(),
	 *            getMaximalSearchDepth()]).
	 */
	public final void setSearchDepthLimit(final int pLimite) {
		assert pLimite >= _minimalSearchDepth;

		_searchDepthLimit = pLimite;
	}
}
